17  向量和 SIMD 简介

在本章中,我想讨论 Zig 中的向量,它们与 SIMD 操作相关(即,它们与std::vectorC++ 中的类没有关系)。

17.1什么是SIMD?

SIMD(单指令/多数据)是一组广泛应用于视频/音频编辑程序以及图形应用程序的操作。SIMD 并非一项新技术,但在普通台式计算机上大规模使用 SIMD 才刚刚开始。过去,SIMD 仅用于“超级计算机”。

如今,大多数现代 CPU(包括 AMD、Intel 等品牌的 CPU)(无论是台式机还是笔记本电脑)都支持 SIMD 操作。因此,如果您的电脑中安装了非常老旧的 CPU,则可能不支持 SIMD 操作。

人们为什么开始在软件中使用 SIMD?答案是为了提高性能。但是,SIMD 究竟是如何实现更佳性能的呢?本质上,SIMD 操作是一种在程序中进行并行计算的策略,从而提高计算速度。

SIMD 背后的基本思想是用一条指令同时对多个数据进行操作。执行普通标量运算时,例如四条加法指令,每个加法指令都是单独执行的,一个接一个。但使用 SIMD 时,这四条加法指令会被转换为一条指令,因此,这四条加法指令可以同时并行执行。

目前,zig编译器允许您对向量对象应用以下一组运算符。当您对向量对象应用这些运算符之一时,将使用 SIMD 进行计算,因此,默认情况下,这些运算符将逐元素并行应用。

  • 算术(+,,,,,,,,等)。-​​​/``*``@divFloor()``@sqrt()``@ceil()``@log()
  • 位运算符(>>,,,,,<<等)。&``|``~
  • 比较运算符(<,,,等)>==

17.2向量

SIMD 操作通常通过_SIMD 内部函数 (intrinsic)_执行,这只是执行 SIMD 操作的函数的别称。这些 SIMD 内部函数(或“SIMD 函数”)始终作用于一种特殊类型的对象,这种对象被称为“向量”。因此,要使用 SIMD,您必须创建一个“向量对象”。

向量对象通常是一个固定大小的 128 位(16 字节)块。因此,您在实际中发现的大多数向量本质上都是数组,包含 2 个 8 字节的值,或者 4 个 4 字节的值,或者 8 个 2 字节的值,等等。但是,不同的 CPU 型号可能具有不同的 SIMD 扩展(或“实现”),它们可能提供更多类型、更大尺寸(256 位或 512 位)的向量对象,以便在单个向量对象中容纳更多数据。

您可以使用@Vector()内置函数在 Zig 中创建一个新的向量对象。在此函数中,您可以指定向量长度(向量中元素的数量)以及向量元素的数据类型。这些向量对象仅支持原始数据类型。在下面的示例中,我创建了两个向量对象(v1v2),每个对象包含 4 个元素u32

另请注意,在下面的示例中,第三个向量对象(v3)是由前两个向量对象(v1plus v2)的和创建的。因此,对向量对象的数学运算默认按元素进行,因为相同的运算(在本例中为加法)被转换为单个指令,并在向量的所有元素上并行复制。

const v1 = @Vector(4, u32){4, 12, 37, 9};
const v2 = @Vector(4, u32){10, 22, 5, 12};
const v3 = v1 + v2;
try stdout.print("{any}\n", .{v3});
{ 14, 34, 42, 21 }

这就是 SIMD 提升程序性能的方式。我们无需使用 for 循环遍历v1和的元素v2,然后一次一个元素地将它们相加,而是可以享受 SIMD 的优势,它可以同时并行执行所有 4 个加法运算。

因此,该@Vector结构本质上是 SIMD 矢量对象的 Zig 表示。当且仅当您当前的 CPU 型号支持 SIMD 操作时,这些矢量对象中的元素才会并行操作。如果您的 CPU 型号不支持 SIMD,那么该@Vector结构可能会产生与“for 循环解决方案”类似的性能。

17.2.1将数组转换为向量

将普通数组转换为矢量对象有多种方法。您可以使用隐式转换(即将数组直接赋值给矢量对象),也可以使用切片从普通数组创建矢量对象。

在下面的例子中,我们隐式地将数组转换a1为长度为 4 的向量对象(v1)。我们首先明确注释向量对象的数据类型,然后将数组对象分配给这个向量对象。

还要注意,在下面的例子中,第二个向量对象(v2)也是通过获取数组对象()的切片a1,然后将指向该切片的指针(.*)存储到该向量对象中来创建的。

const a1 = [4]u32{4, 12, 37, 9};
const v1: @Vector(4, u32) = a1;
const v2: @Vector(2, u32) = a1[1..3].*;
_ = v1; _ = v2;

值得强调的是,只有编译时大小已知的数组和切片才能转换为向量。向量通常是一种仅在编译时大小已知的情况下才能工作的结构。因此,如果您有一个运行时大小已知的数组,那么在将其转换为向量之前,您需要先将其复制到一个编译时大小已知的数组中。

17.2.2函数@splat()

您可以使用@splat()内置函数创建一个向量对象,该对象的所有元素都填充相同的值。此函数旨在提供一种快速简便的方法,将标量值(即单个值,例如单个字符或单个整数等)直接转换为向量对象。

因此,我们可以用@splat()它将单个值(例如整数)转换16为长度为 1 的向量对象。但我们也可以使用此函数将同一个整数转换16为长度为 10 的向量对象,即填充 10 个16值。下面的示例演示了这个想法。

const v1: @Vector(10, u32) = @splat(16);
try stdout.print("{any}\n", .{v1});
{ 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }

17.2.3小心过大的向量

正如我在17.2 节中所述,每个向量对象通常都是 128、256 或 512 位的小块。这意味着向量对象通常很小,而当你试图反其道而行之时,通过创建一个非常大的向量对象(即,大小接近220),通常会导致编译器崩溃和出现严重错误。

例如,如果您尝试编译下面的程序,则可能会在构建过程中遇到段错误或 LLVM 错误。请注意不要创建过大的向量对象。

const v1: @Vector(1000000, u32) = @splat(16);
_ = v1;
Segmentation fault (core dumped)